面試常被問的經典問題:「使用者打一支 API 進來,中間伺服器做了哪些事?」這其實就是在問完整的請求生命週期。以 Spring Boot 來說,大致是這樣:
Client 發出請求
↓
Filter(Servlet 層攔截,例如記錄 log、CORS、驗證 Token)
↓
DispatcherServlet(Spring MVC 的總入口,負責分派請求)
↓
HandlerMapping(比對 URL,找出該由哪個 Controller 方法處理)
↓
Interceptor(Spring 層攔截,例如權限檢查、埋點)
↓
Controller(接收請求參數,呼叫 Service)
↓
Service(商業邏輯,呼叫 Repository)
↓
Repository / JPA(跟資料庫互動)
↓
Controller 組裝回應
↓
Interceptor → Filter(依序反向執行)
↓
回應送回 Client
這篇先聚焦在「入口」——Client 的請求怎麼被對應到某一個 Controller 方法(HandlerMapping 這一段)。中間的 Filter/Interceptor 攔截細節會在 Day 12 展開,Service 層實際的商業邏輯分層則是 Day 09 的主題。把這張全貌圖放在這裡,是希望讀者先有整體地圖,後面兩天再各自深入,不會覺得內容東一塊西一塊。
Laravel 的路由是集中定義的,routes/api.php 裡一行對應一個 Controller 方法:
// routes/api.php
Route::get('/links/{code}', [LinkController::class, 'show']);
Route::post('/links', [LinkController::class, 'store']);
Spring Boot 沒有集中的路由檔,路由資訊直接用註解標在 Controller 方法上:
@RestController
@RequestMapping("/api/links")
class LinkController {
@GetMapping("/{code}")
ResponseEntity<LinkResponse> show(@PathVariable String code) { ... }
@PostMapping
ResponseEntity<LinkResponse> store(@RequestBody CreateLinkRequest request) { ... }
}
這是兩邊思維上最大的轉換:Laravel 要理解一支 API,得先去 routes/api.php 查它對應哪個方法;Spring Boot 反過來,路由資訊就寫在方法本身,不用另外查表,但代價是「這個專案總共有哪些 API」沒有一個地方能一次看完,要看 IDE 的 Endpoints 面板或自己搜尋。
路徑參數:Laravel 靠路由裡的 {code} 佔位符,自動綁到方法參數同名的變數;Spring Boot 一樣寫 {code},但方法參數要明確標 @PathVariable 才會知道「這個參數是從路徑抓的」,不是靠參數名稱猜。
Query 參數:Laravel 用 $request->query('foo') 手動從 Request 物件拿;Spring Boot 用 @RequestParam String foo 直接宣告在方法簽章上,等於是把「這支 API 吃哪些 query 參數」寫死在方法介面裡,一看方法簽章就知道,不用再翻方法內部的程式碼。
Route Model Binding:這是 Laravel 特有的語法糖,Spring Boot 沒有直接對應——Laravel 可以直接把路由參數綁定成 Model 實例,框架自動幫你查資料庫、查不到直接回 404:
Route::get('/links/{link}', [LinkController::class, 'show']);
public function show(Link $link) { ... } // $link 已經是查好的 Eloquent Model
Spring Boot 沒有這層自動化,@PathVariable 拿到的永遠只是原始的字串/數字,查資料庫這件事要自己在方法內呼叫 Repository(Day07 開始會接上)。這不是 Spring Boot 少做了什麼,是兩邊對「Controller 該做多少事」的設計哲學不同:Laravel 傾向讓 Controller 更「薰陶」、少寫幾行;Spring Boot 傾向讓每一步都寫明確,換取更好追蹤。
先只把 Controller 層搭起來,Service/Repository 還沒接(Day07 開始才會真的碰資料庫),這裡的重點是驗證前面講的 routing 機制真的能收到請求、回應:
record CreateLinkRequest(String url) {}
record LinkResponse(String code, String shortUrl) {}
@RestController
@RequestMapping("/api/links")
class LinkController {
@PostMapping
ResponseEntity<LinkResponse> store(@RequestBody CreateLinkRequest request) {
// TODO Day07 開始接上真正的 Service/Repository
// TODO Day14 才會講怎麼真正產生短碼,這裡先寫死示範
String code = "abc123";
return ResponseEntity.ok(new LinkResponse(code, "https://short.ly/" + code));
}
@GetMapping("/{code}")
ResponseEntity<LinkResponse> show(@PathVariable String code) {
// TODO Day07 開始接上真正的 Service/Repository
return ResponseEntity.ok(new LinkResponse(code, "https://short.ly/" + code));
}
}
對照本篇開頭那張生命週期全貌圖,現在打一支 POST /api/links 進來,實際發生的事是:
Client 發出請求
↓
Filter (這篇還沒接,先跳過)
↓
DispatcherServlet
↓
HandlerMapping ← 靠 @RequestMapping("/api/links") + @PostMapping,找到 store() 方法
↓
Interceptor (這篇還沒接,先跳過)
↓
Controller ← store() 執行,@RequestBody 把 JSON 轉成 CreateLinkRequest
↓
Service/Repository(還沒接,先寫死回傳)
↓
Controller 組裝回應(LinkResponse)
↓
回應送回 Client
這篇真正走通的只是「入口」那一段——URL 怎麼被對應到方法、參數怎麼被解析。中間打星號跳過的部分,會依序在後面的 Day 補上:Filter/Interceptor 在 Day12,真正的短碼產生邏輯在 Day14,Service/Repository 分層在 Day09、Day07。